﻿// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- 
// XMLocalizator 0.9
// This code is under The MIT Licence (http://xmlocalizator.codeplex.com/license).
// Copyright (c) 2011 Bartosz Lenar (http://bartoszlenar.com/).
//
// For the full list of people involved in this project, visit:
// http://xmlocalizator.codeplex.com/wikipage?title=About
//
// The latest release, documentation, examples and demo applications always available on:
// http://xmlocalizator.codeplex.com/
// ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- 

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml.Serialization;

using XMLocalizator;
using XMLocalizator.Exceptions;
using XMLocalizator.XmlElements;

namespace XMLocalizator
{
    /// <summary>
    /// The main class of the XMLocalizator. 
    /// Using an object of this class you can load XML file with localization, subscribe other objects and localize them with a single method call.
    /// </summary>
    public class Localizator
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="Localizator"/> class.
        /// </summary>
        public Localizator()
        {
            // set default behaviour of Localizer which occurs when trying to get a non existing phrase
            WhatToDoIfPhraseNotFound = PhraseNotFoundBehavior.ThrowException;

            // set 1 MiB as the maximum size of a XML file with localization
            _XmlFileSizeLimit = 1L * 1024L * 1024L;

            // prepare XmlSerializer
            _MainSerializationType = typeof(Localization);
            _ExtraSerializationTypes = new Type[] { typeof(Phrase) };
            _LocalizationSerializer = new XmlSerializer(_MainSerializationType, _ExtraSerializationTypes);

            _SubscribtionList = new List<ILocalizatorSubscriber>();            
            _TemplatePhrase = new Phrase();
        }

        /// <summary>
        /// Loads localization from the XML file and instantly localizes all subscribed objects.
        /// </summary>
        /// <param name="filePath">The path of the XML file with localization.</param>
        public void LoadAndLocalize(String filePath)
        {
            Load(filePath);
            Localize();
        }

        /// <summary>
        /// Loads localization from the XML file.
        /// </summary>
        /// <param name="filePath">The path of the XML file with localization.</param>
        public void Load(String filePath)
        {
            _Localization = ParseLocalization(filePath);
        }

        /// <summary>
        /// Localizes all subscribed objects.
        /// </summary>
        public void Localize()
        {
            if (_Localization == null)
            {
                throw new NoLocalizationLoadedException("Cannot localize!");
            }

            foreach (ILocalizatorSubscriber s in _SubscribtionList)
            {
                s.Localize();
            }
        }        


        #region LOCALIZATION DATA
        /// <summary>
        /// Gets the phrase with the specified key value.
        /// </summary>
        public String this[String key]
        {
            get
            {
                return GetPhrase(key);
            }
        }

        /// <summary>
        /// Gets the phrase with the specified id.
        /// </summary>
        public String this[int id]
        {
            get
            {
                return GetPhrase(id);
            }
        }

        /// <summary>
        /// Gets the phrase with the specified key value.
        /// </summary>
        /// <param name="key">The key value.</param>
        /// <returns>Localized phrase.</returns>
        public String GetPhrase(String key)
        {
            if (_Localization == null)
            {
                throw new NoLocalizationLoadedException(String.Format("Cannot get phrase with key '{0}'", key));
            }
            
            _TemplatePhrase.Key = key;
            int phraseIndex = _Localization.Phrases.BinarySearch( _TemplatePhrase );

            if (phraseIndex > 0)
            {
                return GetPhraseValue(_Localization.Phrases[phraseIndex]);
            }
            else
            {
                return PhraseNotFoundAction(WhatToDoIfPhraseNotFound, "Key", key);
            }            
        }

        /// <summary>
        /// Gets the phrase with the specified id.
        /// </summary>
        /// <param name="id">The id.</param>
        /// <returns>Localized phrase.</returns>
        public String GetPhrase(int id)
        {
            if (_Localization == null)
            {
                throw new NoLocalizationLoadedException(String.Format("Cannot get phrase with Id '{0}'", id));
            }

            foreach (Phrase phrase in _Localization.Phrases)
            {
                if (phrase.Id == id)
                {
                    return GetPhraseValue(phrase);
                }
            }

            return PhraseNotFoundAction(WhatToDoIfPhraseNotFound, "Id", id.ToString());
        }

        /// <summary>
        /// Gets the currently loaded localization's language.
        /// </summary>
        public String Language
        {
            get
            {
                if (_Localization == null)
                {
                    throw new NoLocalizationLoadedException("Cannot get 'Language' info.");
                }

                return _Localization.Language;
            }
        }

        /// <summary>
        /// Gets the tag of the currently loaded localization's language.
        /// </summary>
        public String LanguageTag
        {
            get
            {
                if (_Localization == null)
                {
                    throw new NoLocalizationLoadedException("Cannot get 'LanguageTag' info.");
                }

                return _Localization.Language;
            }
        }

        /// <summary>
        /// Gets the author(s) of the currently loaded localization.
        /// </summary>
        public String Author
        {
            get
            {
                if (_Localization == null)
                {
                    throw new NoLocalizationLoadedException("Cannot get 'Author' info.");
                }

                return _Localization.Author;
            }
        }

        /// <summary>
        /// Gets the name of the application which the currently loaded localization is created for.
        /// </summary>
        public String Application
        {
            get
            {
                if (_Localization == null)
                {
                    throw new NoLocalizationLoadedException("Cannot get 'Application' info.");
                }

                return _Localization.Application;
            }
        }

        /// <summary>
        /// Gets the version of the application which the currently loaded localization is created for.
        /// </summary>
        public String ApplicationVersion
        {
            get
            {
                if (_Localization == null)
                {
                    throw new NoLocalizationLoadedException("Cannot get 'ApplicationVersion' info.");
                }

                return _Localization.ApplicationVersion;
            }
        }

        /// <summary>
        /// Gets the version of the currently loaded localization.
        /// </summary>
        public String LocalizationVersion
        {
            get
            {
                if (_Localization == null)
                {
                    throw new NoLocalizationLoadedException("Cannot get 'LocalizationVersion' info.");
                }

                return _Localization.LocalizationVersion;
            }
        }

        /// <summary>
        /// Gets the custom data placed in the currently loaded localization.
        /// </summary>
        public String CustomData
        {
            get
            {
                if (_Localization == null)
                {
                    throw new NoLocalizationLoadedException("Cannot get 'CustomData' info.");
                }

                return _Localization.CustomData;
            }
        }

        /// <summary>
        /// Gets amount of the phrases in the currently loaded localization.
        /// </summary>
        public int PhrasesCount
        {
            get
            {
                if (_Localization == null)
                {
                    throw new NoLocalizationLoadedException("Cannot get 'PhrasesCount' info.");
                }

                return _Localization.Phrases.Count;
            }
        }
        #endregion


        #region SUBSCRIPTION       
        /// <summary>
        /// Subscribes the specified object.
        /// </summary>
        /// <param name="subscriber">The object to subscribe.</param>
        public void Subscribe(ILocalizatorSubscriber subscriber)
        {
            if (subscriber == null)
            {
                throw new ArgumentNullException("The 'subscriber' is null!");
            }

            if (IsSubscriberOnTheList(subscriber) == true)
            {
                return;
            }

            _SubscribtionList.Add(subscriber);
        }

        /// <summary>
        /// Unsubscribes the specified object.
        /// </summary>
        /// <param name="subscriber">The object to unsubscribe.</param>
        public void Unsubscribe(ILocalizatorSubscriber subscriber)
        {
            if (subscriber == null)
            {
                throw new ArgumentNullException("The 'subscriber' is null!");
            }

            if (IsSubscriberOnTheList(subscriber) == false)
            {
                return;
            }

            _SubscribtionList.Remove(subscriber);
        }

        /// <summary>
        /// Clears the subscription list.
        /// </summary>
        public void ClearSubscriptions()
        {
            _SubscribtionList.Clear();
        }
        #endregion


        /// <summary>
        /// Gets or sets the Localizer's behavior which occurs when trying to obtain a non existing phrase.
        /// </summary>
        /// <value>
        /// Enum with all available behaviours.
        /// </value>
        public PhraseNotFoundBehavior WhatToDoIfPhraseNotFound { get; set; }

        // ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
        // ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
        // ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
        // ---------- ---------- !! !! !! !!  PRIVATE PART BELOW !! !! !! !! ---------- ---------- 
        // ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
        // ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- 
        // ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- 

        /// <summary>
        /// Determines whether the specified object is subscribed.
        /// </summary>
        /// <param name="subscriber">The object.</param>
        /// <returns>
        ///   <c>true</c> if the specified object is subscribed; otherwise, <c>false</c>.
        /// </returns>
        private bool IsSubscriberOnTheList(ILocalizatorSubscriber subscriber)
        {
            foreach (ILocalizatorSubscriber s in _SubscribtionList)
            {
                if (Object.ReferenceEquals(s, subscriber) == true)
                {
                    return true;
                }
            }

            return false;
        }

        /// <summary>
        /// Return answer to the 'phrase not found' situation.
        /// </summary>
        /// <param name="behavior">The expected behaviour.</param>
        /// <param name="searchedBy">'Key' or 'Id'</param>
        private String PhraseNotFoundAction(PhraseNotFoundBehavior behavior, String searchedBy, String value)
        {
            switch (behavior)
            {
                case PhraseNotFoundBehavior.ThrowException:
                    throw new PhraseNotFoundException(String.Format("There is no phrase with {0} '{1}' in the collection!", searchedBy, value));

                case PhraseNotFoundBehavior.ReturnErrorString:
                    return String.Format("ERROR! {0}='{1}'", searchedBy, value);

                case PhraseNotFoundBehavior.ReturnEmptyString:
                    return String.Empty;

                //case PhraseNotFoundBehaviour.ReturnNull:
                //    return null;
            }

            return null;
        }

        /// <summary>
        /// Gets the phrase's value.
        /// </summary>
        /// <param name="phrase">The phrase.</param>
        /// <returns>Phrase's value.</returns>
        private String GetPhraseValue(Phrase phrase)
        {
            if (String.IsNullOrEmpty(phrase.Value) == true)
            {
                return phrase.Key;
            }
            else
            {
                return phrase.Value;
            }
        }

        /// <summary>
        /// Parses the specified file path, and deserializes the XML file to a Localization object.
        /// </summary>
        /// <param name="filePath">The path of the XML file with localization.</param>
        /// <returns>Deserialized XML file as an equivalent Localization object.</returns>
        private Localization ParseLocalization(String filePath)
        {
            FileInfo file = ParseFileInfo(filePath);

            Localization localization = null;

            try
            {
                using (Stream xmlFile = new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.None))
                {
                    using (StreamReader streamReader = new StreamReader(xmlFile, Encoding.UTF8))
                    {
                        localization = (Localization)(_LocalizationSerializer.Deserialize(streamReader));
                    }                    
                }
            }
            catch (Exception exception)
            {
                throw new InvalidXmlFileException(String.Format("An error occured while loading XML file from path '{0}'.", file.FullName), exception);
            }

            if (String.IsNullOrEmpty(localization.Language) == true)
            {
                throw new InvalidXmlFileException("The 'Language' field is empty!");
            }
            else if (localization.Phrases.Count == 0)
            {
                throw new InvalidXmlFileException("There are no phrases!");
            }

            for (int i = 0; i < localization.Phrases.Count; ++i)
            {
                for (int j = 0; j < localization.Phrases.Count; ++j)
                {
                    if (i == j)
                    {
                        continue;
                    }

                    if (localization.Phrases[i].Id == localization.Phrases[j].Id)
                    {
                        throw new InvalidXmlFileException(String.Format("Duplication of the phrase with id '{0}'!", localization.Phrases[i].Id));
                    }
                    else if (localization.Phrases[i].Key.Equals(localization.Phrases[j].Key) == true)
                    {
                        throw new InvalidXmlFileException(String.Format("Duplication of the phrase with key '{0}'!", localization.Phrases[i].Key));
                    }
                }
            }
            
            localization.Phrases.Sort();

            return localization;
        }

        /// <summary>
        /// Parses the specified path and builds a FileInfo object.
        /// </summary>
        /// <param name="filePath">The file path.</param>
        /// <returns>FileInfo of the specified path.</returns>
        private FileInfo ParseFileInfo(String filePath)
        {
            FileInfo file;

            try
            {
                file = new FileInfo(filePath);
            }
            catch (Exception exception)
            {
                throw new InvalidXmlFileException(String.Format("Cannot create the FileInfo object using path '{0}'. Is the path valid?", filePath), exception);
            }

            if (file.Exists == false)
            {
                throw new InvalidXmlFileException(String.Format("There is no file like '{0}'", file.FullName));
            }
            else if (file.Length > _XmlFileSizeLimit)
            {
                throw new InvalidXmlFileException(String.Format("The size of the file '{0}' ({1} bytes) is above the limit ({2} bytes).", file.FullName, file.Length, _XmlFileSizeLimit));
            }

            return file;
        }


        private Phrase _TemplatePhrase;

        private readonly long _XmlFileSizeLimit;
        private readonly Type _MainSerializationType;
        private readonly Type[] _ExtraSerializationTypes;
        private readonly XmlSerializer _LocalizationSerializer;

        private Localization _Localization;
        private List<ILocalizatorSubscriber> _SubscribtionList;        

    }
}
